﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Objects;
using System.Linq;
using System.Text;
using Contracts;
using System.ServiceModel;

namespace Service
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, // One instance per client
    ConcurrencyMode = ConcurrencyMode.Single)] // One call per session
    public class Service : Contracts.IServer
    {
        static Dictionary<string, IClient> allUsersCallback = new Dictionary<string, IClient>();
        private IClient callback;
        private User user;
        private List<Contact> Contacts;


        /// <summary>
        /// 
        /// </summary>
        /// <param name="mail"></param>
        /// <param name="inputPassword"></param>
        public void Login(string mail, string inputPassword)
        {
            // Get the callback
            callback = OperationContext.Current.GetCallbackChannel<IClient>();

            // Normalize the mail, to avoid differences from 'kikoo@isp.com' and 'kIkOo@IsP.cOm'
            mail = mail.ToLower();

            using(kimEntities entities = new kimEntities())
            {
                UserDB myUser = (from user in entities.UserDBSet
                          where user.mail.Equals(mail)
                          select user).FirstOrDefault();

                if(myUser == null)
                {
                    this.callback.ReceptError("Invalid user !");
                    OperationContext.Current.Channel.Close();
                    return;
                }

                if(! myUser.password.Equals(inputPassword))
                {
                    this.callback.ReceptError("Invalid password !");
                    OperationContext.Current.Channel.Close();
                    return;
                }

                if(! myUser.status.Equals((int)Status.Disconnected))
                {
                    this.callback.ReceptError(string.Format("The user '{0}' is already connected. Please disconnect the other instance and retry.", mail));
                    OperationContext.Current.Channel.Close();
                    return;
                }

                // Register the user
                this.user = new User()
                {
                    Mail = myUser.mail,
                    Nickname = myUser.nickname,
                    Password = myUser.password,
                    Status = Status.Connected
                };
                // Change the status in the db
                myUser.status = (int)Status.Connected;
                entities.SaveChanges();
            }

            // Add the callback to the list
            lock (allUsersCallback)
            {
                allUsersCallback.Add(mail, callback);
            }
            // We want to handle this event to disconnet the user cleanly
            OperationContext.Current.Channel.Closing += new EventHandler(Channel_Closing);

            // Notify the contacts that the user is connected
            this.NotifyUserChange();

            Console.WriteLine("The user '{0}' is now logged in.", this.user.Mail);
            return;
        }

        public void Logout()
        {
            lock (allUsersCallback)
            {
                // Remove this user callback
                allUsersCallback.Remove(user.Mail);
            }

            // Change the status to disconnected
            this.user.Status = Status.Disconnected;
            // Save it to the db
            this.SaveUser();
            // Send the notification of disconnection
            this.NotifyUserChange();
            Console.WriteLine("The user '{0}' is now logged off.", this.user.Mail);
        }

        public bool AddUser(string newMail, string newPassword)
        {
            // Get the callback
            callback = OperationContext.Current.GetCallbackChannel<IClient>();

            // Normalize the mail, to avoid differences from 'kikoo@isp.com' and 'kIkOo@IsP.cOm'
            newMail = newMail.ToLower();

            // Check the mail/password
            if(newMail.Length.Equals(0))
            {
                callback.ReceptError("The mail is empty!");
                return false;
            }
            if(newPassword.Length.Equals(0))
            {
                callback.ReceptError("The password is empty!");
                return false;
            }

            using (kimEntities entities = new kimEntities())
            {
                // Check if this mail is free to use
                UserDB user = (from u in entities.UserDBSet
                               where u.mail.Equals(newMail)
                               select u).FirstOrDefault();
                if( user != null)
                {
                    callback.ReceptError(string.Format("The mail '{0}' is already used. Please choose another mail.", newMail));
                    return false;
                }

                // Add the user
                UserDB newUser = new UserDB()
                                     {
                                         mail = newMail,
                                         password = newPassword,
                                         nickname = "",
                                         status = (int) Status.Disconnected
                                     };

                entities.AddToUserDBSet(newUser);
                entities.SaveChanges();

                this.addGroup("Friends", newUser.mail);
                this.addGroup("Familly", newUser.mail);
                this.addGroup("Work", newUser.mail);
            }
            return true;
        }

        private void addGroup(string group, string mail)
        {
            using (kimEntities entities = new kimEntities())
            {
                // select the users
                UserDB user = (from u in entities.UserDBSet
                               where u.mail.Equals(mail)
                               select u).FirstOrDefault();
                if (user == null)
                    return;

                GroupDB newGroup = new GroupDB()
                              {
                                  name = group,
                                  users = user
                              };
                entities.AddToGroupDBSet(newGroup);
                entities.SaveChanges();
            }
        }

        private void Channel_Closing(object sender, EventArgs e)
        {
            lock (allUsersCallback)
            {
                // If the user is still logged...
                if (allUsersCallback.ContainsKey(this.user.Mail))
                {
                    Console.WriteLine("The user '{0}' terminated the session unexpectly !", this.user.Mail);
                    this.Logout();
                }
            }
        }

        public void SendMessage(string to, string message)
        {
            Message theMessage = new Message()
                                     {
                                         Text = message,
                                         Date = DateTime.Now,
                                         From = this.UserToContact(this.user)
                                     };
            lock(allUsersCallback)
            {
                // If the recipient is logged in, send it now
                if(allUsersCallback.ContainsKey(to))
                {
                    allUsersCallback[to].ReceptMessage(theMessage);
                }
                // Else, save it to the db
                else
                {
                    this.SaveMessage(to, theMessage);
                }
            }
        }

        private void SaveMessage(string to, Message theMessage)
        {
            using (kimEntities entities = new kimEntities())
            {
                UserDB recipient = (from u in entities.UserDBSet
                                    where u.mail.Equals(to)
                                    select u).FirstOrDefault();
                UserDB sender = (from u in entities.UserDBSet
                                where u.mail.Equals(theMessage.From.Mail)
                                select u).FirstOrDefault();

                MessageDB dbMessage = new MessageDB()
                {
                    message = theMessage.Text,
                    date_msg = theMessage.Date,
                    recipient_user = recipient,
                    sender_user = sender
                };

                entities.AddToMessageDBSet(dbMessage);
                entities.SaveChanges();
            }
        }

        public User GetUser()
        {
            return this.user;
        }

        public void UpdateUser(User newUser)
        {
            // Update the user
            this.user.Nickname = newUser.Nickname;
            this.user.Status = newUser.Status;
            this.user.Password = newUser.Password;

            // Save it to the db
            this.SaveUser();

            this.NotifyUserChange();
        }

        public void UploadAvatar(byte[] avatar)
        {
            using (kimEntities entities = new kimEntities())
            {
                UserDB user = (from u in entities.UserDBSet
                               where u.mail.Equals(this.user.Mail)
                               select u).FirstOrDefault();
                user.img = avatar;
                entities.SaveChanges();
            }
        }

        public byte[] DownloadAvatar(string mail)
        {
            using (kimEntities entities = new kimEntities())
            {
                UserDB user = (from u in entities.UserDBSet
                               where u.mail.Equals(mail)
                               select u).FirstOrDefault();
                if(user == null)
                {
                    this.callback.ReceptError(string.Format("The user '{0}' does not exist!", mail));
                    return null;
                }
                return user.img;
            }
        }

        public List<string> GetGroupList()
        {
            using (kimEntities entities = new kimEntities())
            {
                return (from g in entities.GroupDBSet
                        where g.users.mail.Equals(this.user.Mail)
                        select g.name).ToList();
            }
        }

        public List<Message> GetUnreadMessages()
        {
            using (kimEntities entities = new kimEntities())
            {
                // Get the messages from the db
                List<MessageDB> listMessageDB = (from m in entities.MessageDBSet.Include("sender_user")
                        where m.recipient_user.mail.Equals(this.user.Mail)
                        select m).ToList();

                // Build the list to return and delete the records from the db
                List<Message> listMessage = new List<Message>();
                foreach(MessageDB messageDB in listMessageDB)
                {
                    Contact from = new Contact()
                                        {
                                            Mail = messageDB.sender_user.mail,
                                            Nickname = messageDB.sender_user.nickname,
                                            StatusInt = messageDB.sender_user.status
                                        };

                    listMessage.Add(new Message()
                                    {
                                        Date = messageDB.date_msg,
                                        From = from,
                                        Text = messageDB.message
                                    });
                    entities.DeleteObject(messageDB);
                }
                entities.SaveChanges();
                return listMessage;
            }
        }

        public List<Contact> GetContactList()
        {
            using (kimEntities entities = new kimEntities())
            {
                var contactList = (from contact in entities.ContactDBSet
                                   where contact.user_user.mail.Equals(this.user.Mail)
                                   select new Contact()
                                              {
                                                  Nickname = contact.contact_user.nickname,
                                                  Alias = contact.alias,
                                                  Blocked = (contact.blocked == 1) ? true : false,
                                                  Mail = contact.contact_user.mail,
                                                  StatusInt = contact.contact_user.status
                                              }).ToList();
                return contactList;
            }
        }

        public bool AddContact(Contact theContact)
        {
            using (kimEntities entities = new kimEntities())
            {
                UserDB contactUser = (from user in entities.UserDBSet
                                       where user.mail.Equals(theContact.Mail)
                                       select user).FirstOrDefault();

                if(contactUser == null)
                {
                    callback.ReceptError(string.Format("Can not add the user '{0}' to your contact list because he is not registered to the KIM server.", theContact.Mail));
                    return false;
                }

                GroupDB contactGroup = (from g in entities.GroupDBSet
                                      where g.name.Equals(theContact.Group) && g.users.mail.Equals(this.user.Mail)
                                      select g).FirstOrDefault();

                if(contactGroup == null)
                {
                    callback.ReceptError(string.Format("The group '{0}' does not exist !", theContact.Group));
                    return false;
                }

                UserDB currentUser = (from user in entities.UserDBSet
                                     where user.mail.Equals(this.user.Mail)
                                         select user).FirstOrDefault();

                // TODO: get rid of this
                if (theContact.Alias == null)
                    theContact.Alias = theContact.Mail;

                ContactDB newContact = new ContactDB()
                                         {
                                             user_user = currentUser, // The owner of the contact
                                             alias = theContact.Alias,
                                             blocked = theContact.Blocked ? 1 : 0,
                                             contact_user = contactUser, // The user which is the contact
                                             groups = contactGroup
                                         };
                entities.AddToContactDBSet(newContact);
                try
                {
                    entities.SaveChanges();
                } catch(UpdateException)
                {
                    callback.ReceptError(string.Format("The user '{0}' is already in your contact list !", theContact.Mail));
                    return false;
                }
                return true;
            }
        }

        public bool EditContact(Contact newContact)
        {
            using (kimEntities entities = new kimEntities())
            {
                ContactDB theContact = (from c in entities.ContactDBSet
                                      where c.user_user.mail.Equals(this.user.Mail) // Owned by the user
                                            && c.contact_user.mail.Equals(newContact.Mail) // The contact id (mail)
                                      select c).FirstOrDefault();
                if(theContact == null)
                {
                    callback.ReceptError(string.Format("The user '{0}' is not in your contact list !", newContact.Mail));
                    return false;
                }

                GroupDB newGroup = (from g in entities.GroupDBSet
                                where g.users.mail.Equals(this.user.Mail)
                                      && g.name.Equals(newContact.Group)
                                select g).FirstOrDefault();

                if(newGroup == null)
                {
                    callback.ReceptError(string.Format("The group '{0}' does not exist !", newContact.Group));
                    return false;
                }

                // Bind the new datas
                theContact.alias = newContact.Alias;
                theContact.blocked = (newContact.Blocked) ? 1 : 0;
                theContact.groups = newGroup;

                // Save
                entities.SaveChanges();
                return true;
            }
        }

        public bool DeleteContact(string mail)
        {
            using (kimEntities entities = new kimEntities())
            {
                ContactDB theContact = (from c in entities.ContactDBSet
                                    where
                                        c.user_user.mail.Equals(this.user.Mail) // The contact is owned by the current user
                                        && c.contact_user.mail.Equals(mail)
                                    select c).FirstOrDefault();

                if(theContact == null)
                {
                    callback.ReceptError(string.Format("The user '{0}' is not in your contact list !", mail));
                    return false;
                }

                entities.DeleteObject(theContact);
                entities.SaveChanges();
                return true;
            }
        }

        // Get the mail of the users who have the current user in their contact list
        private List<string> GetUsersMailFromCurrent()
        {
            using (kimEntities entities = new kimEntities())
            {
                return (from contact in entities.ContactDBSet
                       where contact.contact_user.mail.Equals(this.user.Mail)
                       select contact.user_user.mail).ToList();
            }
        }

        // Notify all the users who have the current user in their contact list that the current user just changed
        private void NotifyUserChange()
        {
            // Get the contact from the user
            Contact theContact = this.UserToContact(this.user);
            // Get the mail of the users who have the current user in their contact list
            List<string> contactsMail = this.GetUsersMailFromCurrent();

            // Notify them
            foreach (string mail in contactsMail)
            {
                lock (allUsersCallback)
                {
                    // If this user is connected
                    if (allUsersCallback.ContainsKey(mail))
                    {
                        allUsersCallback[mail].ReceptContactChange(theContact);
                    }
                }
            }
        }

        // Create a Contact object from an User one
        private Contact UserToContact(User user)
        {
            return new Contact()
            {
                Mail = user.Mail,
                Nickname = user.Nickname,
                Status = user.Status
            };
        }

        // Save the current user state to the db
        private void SaveUser()
        {
            using (kimEntities entities = new kimEntities())
            {
                // Get the user as it is known in the db
                UserDB oldUser = (from u in entities.UserDBSet
                                where u.mail.Equals(this.user.Mail)
                                select u).FirstOrDefault();

                // Change the datas with the current state
                oldUser.nickname = this.user.Nickname;
                oldUser.password = this.user.Password;
                oldUser.status = (int)this.user.Status;

                // Save it
                entities.SaveChanges();
            }
        }

        // Disconnect all users, used at startup time
        public static void DisconnectAllUsers()
        {
            using (kimEntities entities = new kimEntities())
            {
                List<UserDB> allUsers = (from u in entities.UserDBSet
                                         select u).ToList();
                foreach (UserDB user in allUsers)
                {
                    user.status = (int)Contracts.Status.Disconnected;
                }
                entities.SaveChanges();
            }
        }
    }
}
